Skip to content

feat(core): Add IsolatedVmBridge (no-changelog)#2089

Draft
everettbu wants to merge 7 commits intofeat/expression-runtime-pr2-runtime-bundlefrom
feat/expression-runtime-pr3-isolated-vm-bridge
Draft

feat(core): Add IsolatedVmBridge (no-changelog)#2089
everettbu wants to merge 7 commits intofeat/expression-runtime-pr2-runtime-bundlefrom
feat/expression-runtime-pr3-isolated-vm-bridge

Conversation

@everettbu
Copy link
Copy Markdown

Mirror of n8n-io/n8n#26142
Original author: despairblue


Context

n8n expressions today run via tournament (an AST transformer) inside the main Node.js process. The goal of this PR series is to make expression evaluation available inside isolated-vm — a V8 isolate that is memory-isolated from the host process. This gives us a proper security boundary: a malicious expression cannot access Node.js APIs, the filesystem, or the host's memory.

The series is being landed incrementally:

PR What it adds
#26047 ✅ Package scaffold: public types and architecture docs
#26077 ✅ The runtime bundle that runs inside the isolate
This PR IsolatedVmBridge — creates the isolate, loads the bundle, and exposes workflow data to it via callbacks
PR 4 (upcoming) ExpressionEvaluator — the public API that drives the bridge, plus integration tests

What this PR adds

IsolatedVmBridge is the component that owns the V8 isolate. It:

  1. Creates a persistent isolate (128 MB memory limit) in the constructor
  2. Loads the runtime bundle (dist/bundle/runtime.iife.js from PR 2) into the isolate context on initialize()
  3. Registers three ivm.Reference callbacks before each evaluation so the in-isolate lazy proxy can fetch workflow data from the host without pre-copying it:
    • __getValueAtPath(path[]) — returns a primitive, array metadata {__isArray, __length}, object metadata {__isObject, __keys}, or function metadata
    • __getArrayElement(path[], index) — returns a single element (or its metadata) from an array
    • __callFunctionAtPath(path[], ...args) — invokes a function at the given path in the host data and returns the result
  4. Calls resetDataProxies() in the isolate to initialise $json, $binary, $input, etc. as fresh lazy proxies backed by those callbacks
  5. Runs the tournament-transformed expression with this === __data, so this.$json.email resolves through the proxy

Arrays are always lazy — only the length is transferred. Elements are fetched individually on demand via __getArrayElement. There is no small-array threshold.

Script compilation is cached — once an expression is compiled to an ivm.Script, it is stored in a Map keyed by the expression string. Subsequent evaluations of the same expression skip recompilation.

Isolation setup in initialize()

Broken into four focused private methods, each independently verifiable in debug mode:

  • loadVendorLibraries() — evaluates the runtime bundle in the isolate context and verifies DateTime and extend are available on globalThis
  • verifyProxySystem() — checks that createDeepLazyProxy, resetDataProxies, SafeObject, SafeError, and __data are all present
  • injectErrorHandler() — injects the E() global needed by tournament's try-catch wrapping: re-throws security violations, swallows TypeErrors (failed prototype-attack attempts), re-throws everything else

New dependency: isolated-vm ^6.0.2

isolated-vm is a native Node.js addon (compiled from C++ at install time). It is added to pnpm.onlyBuiltDependencies in the root package.json so pnpm install compiles it correctly.

Why no tests in this PR?

The interesting behaviour of this bridge is the round-trip: isolate creates a proxy → proxy fires an ivm.Reference callback → callback navigates host-side data → value is copied back into the isolate. Unit tests with mocked callbacks would only verify that a mock was called, not the actual serialisation, copy: true transfer semantics, or the __isArray/__isObject metadata protocol.

Integration tests covering the full path (bridge creates isolate → loads runtime bundle → sets callbacks → expression evaluates to the correct result) are planned for PR 4, once ExpressionEvaluator exists to drive the bridge.

Verification

cd packages/`@n8n`/expression-runtime
pnpm build       # tsc + esbuild — both succeed
pnpm typecheck   # passes

Related Linear tickets, Github issues, and Community forum posts

https://linear.app/n8n/issue/CAT-2311

Review / Merge checklist

  • PR title and summary are descriptive. (conventions)
  • Docs updated or follow-up ticket created.
  • Tests included. (deferred to PR 4 — see note above)
  • PR Labeled with release/backport (if the PR is an urgent fix that needs to be backported)

despairblue and others added 7 commits February 23, 2026 16:53
…dependency

Introduces the IsolatedVmBridge class that creates a persistent V8 isolate,
loads the runtime bundle into it, and exposes three synchronous ivm.Reference
callbacks (__getValueAtPath, __getArrayElement, __callFunctionAtPath) for
cross-isolate data access. Adds isolated-vm ^6.0.2 as a native dependency.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…atedVmBridge

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ivate method

Consistent with loadVendorLibraries() and defineProxySystem() pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…endorLibraries

_ and luxon are not exposed on globalThis (removed in PR 2). Check DateTime
and extend instead, which are the actual globals the runtime bundle exports.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…wrappers from callbacks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…th working doc

- Remove getDataSync: not in RuntimeBridge interface, no callers
- Remove small-array threshold: arrays are always lazy-loaded (length only)
- Rename defineProxySystem -> verifyProxySystem (method only verifies)
- Update stale comments referencing removed _ and luxon globals
- Update initialize() JSDoc steps to reflect current implementation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tate

- README: mark PR 2 (runtime bundle) and PR 3 (IsolatedVmBridge) as complete
- README: remove stale "(PR 3)" annotations from BridgeConfig
- deep-lazy-proxy.md: fix runtime module reference (lazy-proxy.ts, not index.ts)
- deep-lazy-proxy.md: expand Related Files to list all four runtime modules
- architecture-diagram.mmd: replace stale dataId/Store sequence with actual
  ivm.Reference callback flow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@everettbu everettbu added the n8n team Authored by the n8n team label Feb 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

n8n team Authored by the n8n team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants